package com.silence.tool.excel.service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.util.StringUtils;
import com.silence.tool.common.NumberConstants;
import com.silence.tool.common.StringConstants;
import com.silence.tool.excel.domain.input.UnpivotInput;
import com.silence.tool.exception.ApiException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @author silence
 * @email 792245022@qq.com
 * @create 2023-08-28 16:25
 * <p>
 * 71 这一行解析数据，使用 easyExcel 下面代码，可以平替上面 POI 解析，至于为什么用 easyExcel，
 * 是因为 easyExcel 解析速度快，而且支持多线程解析。但是为了算法观看，并且 easyExcel 矩阵数据必须都有数据，故就用了 POI 解析
 * if (!StringConstants.XLSX.equalsIgnoreCase(substring) && !StringConstants.XLS.equalsIgnoreCase(substring)) {
 * return Pair.of(false, "文件格式不正确");
 * }
 * Triple<Boolean, String, List<List<String>>> triple = readExcelDataParseByEasyExcel(sourceFile);
 * //  校验结果
 * if (!triple.getLeft()) {
 * return Pair.of(false, triple.getMiddle());
 * }
 * </p>
 */
@Slf4j
@Service
public class ExcelService {

    /**
     * 行列转换
     *
     * @param input 入参
     * @return 结果
     */
    public Pair<Boolean, String> unpivot(UnpivotInput input) {
        log.info("input: {}", input);
        // 获取源路径以及目标路径
        String sourceFilePath = input.getSourceFilePath().replace("\\", "\\\\").trim();
        log.info("源路径: {}", sourceFilePath);
        // 源文件校验
        File sourceFile = new File(sourceFilePath);
        Pair<Boolean, String> sourceFileCheck = sourceFileCheck(sourceFile);
        if (!sourceFileCheck.getLeft()) {
            return Pair.of(false, sourceFileCheck.getRight());
        }
        // 截取最后尾缀
        String substring;
        try {
            substring = sourceFilePath.substring(sourceFilePath.lastIndexOf(".") + 1);
        } catch (Exception e) {
            log.warn("文件路径尾缀格式不正确", e);
            return Pair.of(false, "文件路径尾缀格式不正确");
        }
        // 校验 Excel 格式
        Triple<Boolean, String, List<List<String>>> triple = switch (substring.toUpperCase()) {
            // xlsx 格式
            case StringConstants.XLSX -> readExcelDataParseByPoi(sourceFile, item -> {
                XSSFWorkbook workbook;
                try {
                    workbook = new XSSFWorkbook(item);
                } catch (IOException e) {
                    log.warn("文件读取失败", e);
                    workbook = null;
                }
                return workbook;
            });
            // xls 格式
            case StringConstants.XLS -> readExcelDataParseByPoi(sourceFile, item -> {
                HSSFWorkbook workbook;
                try {
                    workbook = new HSSFWorkbook(item);
                } catch (IOException e) {
                    log.warn("文件读取失败", e);
                    workbook = null;
                }
                return workbook;
            });
            // 文件格式不正确
            default -> Triple.of(false, "文件格式不正确", null);
        };
        //  校验结果
        if (!triple.getLeft()) {
            return Pair.of(false, triple.getMiddle());
        }
        // 获取解析结果
        List<List<String>> allDataList = triple.getRight();
        log.info("解析读取结果：{}", allDataList);
        if (CollectionUtils.isEmpty(allDataList)) {
            return Pair.of(false, "解析读取结果为空");
        }
        // 行列转换
        List<List<String>> unpivotList;
        try {
            unpivotList = rowsOrColsConversion(allDataList, input.getRowNum(), input.getColNum());
        } catch (ApiException e) {
            return Pair.of(false, e.getMsg());
        }
        log.info("行列转换结果：{}", unpivotList);
        // 获取目标路径
        String targetFilePath = input.getTargetFilePath();
        if (StringUtils.isBlank(targetFilePath)) {
            targetFilePath = sourceFilePath.substring(0, sourceFilePath.lastIndexOf(".")) +
                    "（new）" + sourceFilePath.substring(sourceFilePath.lastIndexOf("."));
        }
        log.info("目标文件路径：{}", targetFilePath);
        File targetFile = new File(targetFilePath);
        // 写出数据
        try {
            EasyExcel.write(targetFile, List.class)
                    .sheet("Sheet1")
                    .doWrite(CollectionUtils.isEmpty(unpivotList) ? new ArrayList<>() : unpivotList.stream()
                            .map(list -> IntStream.range(0, list.size())
                                    .boxed()
                                    .collect(Collectors.toMap(Function.identity(), list::get, (k1, k2) -> k2)))
                            .collect(Collectors.toList()));
        } catch (Exception e) {
            log.warn("文件写出失败", e);
            return Pair.of(false, "文件写出失败");
        }
        log.info("文件转换完成");
        return Pair.of(true, StringConstants.SUCCESS);
    }

    /**
     * 源文件校验
     *
     * @param sourceFile 源文件
     * @return 结果
     */
    private Pair<Boolean, String> sourceFileCheck(File sourceFile) {
        if (!sourceFile.exists()) {
            return Pair.of(false, "源文件不存在");
        }
        if (!sourceFile.isFile()) {
            return Pair.of(false, "源文件不是文件");
        }
        if (!sourceFile.canRead()) {
            return Pair.of(false, "源文件不可读");
        }
        if (sourceFile.length() >= NumberConstants.MAX_30M_EXCEL_SIZE) {
            return Pair.of(false, "源文件大小超出限制");
        }
        return Pair.of(true, StringConstants.SUCCESS);
    }

    /**
     * 通过 POI 读取 Excel 文件数据
     *
     * @param sourceFile 源文件
     * @param function   函数式接口
     * @return 数据结果
     */
    @SneakyThrows
    private Triple<Boolean, String, List<List<String>>> readExcelDataParseByPoi(File sourceFile, Function<FileInputStream, ? extends Workbook> function) {
        // EasyExcel 读取
        FileInputStream fileInputStream = new FileInputStream(sourceFile);
        Workbook wb = function.apply(fileInputStream);
        if (Objects.isNull(wb)) {
            log.warn("源文件IO异常");
            return Triple.of(false, "源文件IO异常", null);
        }
        // 要转置的第几个sheet页（默认第一个 sheet 页，可以自定义）
        Sheet sheet = wb.getSheetAt(0);
        // getLastRowNum 返回的是最后一行的索引而不是最后一行的行数，getLastCellNum则是返回的最后一列的列数。
        int rowNum = sheet.getLastRowNum() + 1;
        if (rowNum <= 0) {
            log.warn("源文件中没有数据");
            return Triple.of(false, "源文件中没有数据", null);
        }
        log.info("源文件中数据行数：{}", rowNum);
        // 创建返回数据
        List<List<String>> allDataList = new ArrayList<>(rowNum);
        // 数据格式化
        DataFormatter dataFormatter = new DataFormatter();
        for (int i = 0; i < rowNum; i++) {
            Row row = sheet.getRow(i);
            int colNum = 0;
            if (Objects.nonNull(row)) {
                colNum = row.getLastCellNum();
            }
            log.info("源文件中数据列数：{}", colNum);
            List<String> rowsDataList = new ArrayList<>(colNum);
            for (int j = 0; j < colNum; j++) {
                // 设置值为字符串
                Cell cell = sheet.getRow(i).getCell(j);
                String value = dataFormatter.formatCellValue(cell);
                rowsDataList.add(value);
            }
            allDataList.add(rowsDataList);
        }
        // 补充集合元素 见（excel补充字符串图片.png）[[], [, 2, 2, 2, 2, , 2], [, 3, 3, 3, 3, , 3, 3], [, 4, 4, 4, 4, , 4, 4, 4]...]
        // 导致数据行数不匹配二维数组集合行数，需要字符串补充，由于 POI 解析获取行最后列，所以不需要补充前元素，后元素用字符串填充
        int maxLength = allDataList.stream()
                .filter(CollectionUtils::isNotEmpty)
                .mapToInt(List::size)
                .max()
                .orElse(0);
        if (maxLength <= 0) {
            return Triple.of(false, "源文件中没有数据", null);
        }
        allDataList.forEach(list -> {
            while (list.size() < maxLength) {
                list.add(StringConstants.EMPTY);
            }
        });
        // 关闭流
        fileInputStream.close();
        wb.close();
        log.info("源文件数据转换完成");
        // 返回结果
        return Triple.of(true, StringConstants.SUCCESS, allDataList);
    }

    /**
     * 通过  EasyExcel 读取 Excel 文件数据
     *
     * @param sourceFile 源文件
     * @return 数据结果
     */
    private Triple<Boolean, String, List<List<String>>> readExcelDataParseByEasyExcel(File sourceFile) {
        List<Map<Integer, String>> easyExcelParseDataList = EasyExcel.read(sourceFile)
                .headRowNumber(0)
                .sheet()
                .doReadSync();
        if (CollectionUtils.isEmpty(easyExcelParseDataList)) {
            return Triple.of(false, "源文件中没有数据", null);
        }
        List<List<String>> allDataList = easyExcelParseDataList.stream()
                .map(item -> item.values().stream().toList())
                .toList();
        return Triple.of(true, StringConstants.SUCCESS, allDataList);
    }

    /**
     * 反转行列
     *
     * @param allDataList 数据
     * @param rowNum      反转行数
     * @param colNum      反转列数
     * @return 反转后结果
     */
    private List<List<String>> rowsOrColsConversion(List<List<String>> allDataList, Integer rowNum, Integer colNum) {
        // 记录所有数据
        int count = allDataList.stream().mapToInt(List::size).sum();
        // 因为上面补充了空行，是行相等列相等的矩阵，所以这里获取第一行的长度就是每行列数
        int size = allDataList.get(0).size();
        // 第一种情况，行列都没有输入，默认将其行列反转
        if (ObjectUtils.allNull(rowNum, colNum) ||
                (ObjectUtils.allNotNull(rowNum, colNum) && rowNum == 0 && colNum == 0)) {
            return IntStream.range(0, size)
                    .mapToObj(i -> allDataList.stream().map(row -> row.get(i)).toList())
                    .toList();
        } else if (Objects.nonNull(rowNum) && Objects.isNull(colNum)) {
            // 第二种情况，只输入了行数，将列数反转到指定行数
            if (rowNum == 0 || rowNum >= count) {
                rowNum = count;
            }
            // 指定的行数
            int rows = rowNum;
            // 指定的列数
            int cols = (int) Math.ceil(count / (double) rows);
            // 取出多出个数的行数
            int excessRows = count % rows;
            log.info("行数：{}，列数：{}, 多出个数行数：{}", rows, cols, excessRows);
            // 拆分变成一维集合
            List<String> toList = IntStream.range(0, size)
                    .mapToObj(i -> allDataList.stream().map(row -> row.get(i)).toList())
                    .flatMap(List::stream)
                    .toList();
            return IntStream.range(0, rows)
                    .mapToObj(row -> {
                        if ((toList.size() % rows == 0) || row + 1 <= excessRows) {
                            return toList.subList(row * cols, (row + 1) * cols);
                        }
                        return toList.subList(row * (cols - 1) + excessRows, (row + 1) * (cols - 1) + excessRows);
                    }).map(item -> {
                        List<String> mapList = new ArrayList<>(item);
                        while (mapList.size() < cols) {
                            mapList.add(StringConstants.EMPTY);
                        }
                        return mapList;
                    }).toList();
        } else if (Objects.isNull(rowNum) && Objects.nonNull(colNum)) {
            // 第三种情况，只输入了列数，将每列进行反转
            if (colNum == 0 || colNum >= count) {
                colNum = count;
            }
            // 指定的列数
            int cols = colNum;
            // 指定的行数
            int rows = (int) Math.ceil(count / (double) cols);
            // 取出多出个数的列数
            int excessCols = count % cols;
            // 拆分变成一维集合
            List<String> toList = allDataList.stream().flatMap(List::stream).toList();
            // 组合将变成列的集合
            List<List<String>> colsList = IntStream.range(0, cols)
                    .mapToObj(col -> {
                        if ((toList.size() % cols == 0) || col + 1 <= excessCols) {
                            return toList.subList(col * rows, (col + 1) * rows);
                        }
                        return toList.subList(col * (rows - 1) + excessCols, (col + 1) * (rows - 1) + excessCols);
                    }).map(item -> {
                        List<String> mapList = new ArrayList<>(item);
                        while (mapList.size() < rows) {
                            mapList.add(StringConstants.EMPTY);
                        }
                        return mapList;
                    }).toList();
            // 构建转置行数组
            return IntStream.range(0, rows)
                    .mapToObj(i -> colsList.stream().map(row -> row.get(i)).toList())
                    .toList();
        } else {
            // 第四种情况，输入了行列数，将指定行列进行反转
            // 指定的行数
            int rows = rowNum;
            // 指定的列数
            int cols = colNum;
            if (rows * cols > count) {
                log.warn("选择行列数大于总数量，行数：{},列数:{}", rows, cols);
                throw new ApiException(500, "选择行列数量大于总数量");
            }
            // 拆分变成一维集合
            List<String> toList = allDataList.stream().flatMap(List::stream).toList();
            // 截取待转换集合
            List<List<String>> mapList = IntStream.range(0, cols)
                    .mapToObj(row -> toList.subList(row * rows, (row + 1) * rows))
                    .toList();
            // 返回结果
           return IntStream.range(0, rows)
                    .mapToObj(i -> mapList.stream().map(row -> row.get(i)).toList())
                    .toList();
        }
    }

}
